Skip to content

fix(desktop): input during window teardown no longer throws in the main process - #33620

Merged
juliusmarminge merged 4 commits into
mainfrom
fix/issue-33614
Nov 10, 2026
Merged

juliusmarminge merged 4 commits into
mainfrom
fix/issue-33614

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Problem

Quitting the desktop app with a held ⌘Q (Confirm quit: Hold) can show a main-process TypeError: Object has been destroyed dialog. A held quit can fire from the grace timer, so the trailing Q key-up reaches the before-input-event listener in apps/desktop/src/window/DesktopWindow.ts while the window is being torn down. That listener reads window.webContents to call setIgnoreMenuShortcuts without an isDestroyed() check, and Electron throws on any member access of a destroyed BrowserWindow. The sibling input-event listener (trackpad scroll begin/end) has the same missing guard.

Change

Both listeners now return early when window.isDestroyed(), the same guard the zoom-changed listener and the other handlers in the file already use. In before-input-event the guard comes after quitShortcutHandler(event, input), so the quit-hold flow still sees the trailing key-up. The guard is on the window, not window.webContents.isDestroyed(), because reading .webContents is itself what throws.

Scope and approval

Fixes #33614

Verification

  • DesktopWindow > ignores input that arrives after the window is destroyed (new, apps/desktop/src/window/DesktopWindow.test.ts) marks the fake window destroyed, makes webContents throw TypeError("Object has been destroyed") as Electron does, then sends a trailing ⌘Q keyUp to before-input-event and a gestureScrollEnd to input-event. It fails on main with AssertionError: expected [Function] to not throw an error but 'TypeError: Object has been destroyed' was thrown, and still fails with only the before-input-event guard. With this change all 28 tests in the file pass.
  • vp run check:changed after merging main: format, lint, knip, typecheck and tests pass.

Evidence

The real T3 desktop app (the built dist-electron/main.cjs with its preload, renderer and bundled server) ran on Linux with Confirm quit set to Hold. Base is main at bbdc7dca and head is a103cf6d; the later head only merges main. Each side ran from its own checkout with fresh, isolated state. The fix has no rendered surface, so the proof is a set of chronological main-process transcripts. The driver captures the listener the app itself registered and calls that retained callback; it never copies the implementation.

Environment: Ubuntu 24.04, Electron 44.10.1 / Chromium 152, headless-desktop with Xvfb 1920×1080, scale 1, T3CODE_DEV_SEED=0.

Row Before (bbdc7dc) After (a103cf6) Result
Native Ctrl+Q held about 1550 ms, then released native-base.log native-head.log Both reach the hold threshold and quit cleanly through the quiet-period timer. Electron under Xvfb delivers no Q key-up callback.
Release route (harness): native hold, then the missing Q key-up forced into the installed listener release-base.log release-head.log Base quits, destroys the window, then throws Object has been destroyed. Head quits and returns cleanly.
Quiet-period route (harness): repeats stop while Q stays down quiet-base.log quiet-head.log On both sides the timer starts shutdown about 920 ms before the physical release. Both exit 0.
before-input-event after real BrowserWindow.destroy(): Q up, Control up, X down listeners-base.log listeners-head.log All three throw on base. All three return on head, with no menu call and no IPC.
input-event after destroy: gestureScrollBegin, gestureScrollEnd, mouseMove same same Begin and end throw on base and return without IPC on head. mouseMove returns on both.
Live window controls: Ctrl+W down, repeat and up; plain W; Ctrl+X; Alt/Shift+Ctrl+W; gesture begin and end same same All ten outcomes are identical on base and head: menu interception, repeat prevention and scroll IPC.

What else changed: only the exceptions after destroy disappear. Outcomes on the live window match exactly (behaviour diff, comparison). Startup timing, repeat cadence and process IDs vary between runs.

Scripts and full index: evidence-index.md, reproduce.sh, run-one.mjs, driver.cjs, native-input.py. Supplemental Xvfb recordings (they include startup; the logs above are the proof): native-base.mp4 / native-head.mp4, release-base.mp4 / release-head.mp4.

cannot-verify macos: the native ⌘Q hold timing was not reproduced on macOS. On Linux, the native trailing key-up never reaches the listener under Xvfb, so the release route is a labelled harness run on the real app.

Verification log
  • Review round 1: two independent reviewers, both MERGE, with no findings. Not a sensitive area and no product decision.
  • Proof audit: pass 1 derived the matrix (matrix). Pass 2 returned SUFFICIENT. It rebuilt the after-side listener capture independently and all 16 case results matched (rebuilt logs).
  • Tests: 28/28 in DesktopWindow.test.ts. The quit-handler suite also passes.
  • Merged main at ad98905; the fix diff is unchanged.

AI-assisted; verified with the tests and tools listed above.

…33614)

Quitting with a held Cmd+Q can tear the window down before the trailing
Q keyUp reaches the before-input-event listener, which then calls
window.webContents.setIgnoreMenuShortcuts on a destroyed window and
throws "TypeError: Object has been destroyed". The sibling input-event
listener has the same missing guard. This test fails on main for both.

AI-assisted; verified with the tests and tools listed above.
…in process

A trailing key-up (e.g. releasing a held Cmd+Q) can reach the window's
before-input-event and input-event listeners after the BrowserWindow is
destroyed. Both read window.webContents unguarded, so Electron threw
"TypeError: Object has been destroyed" as an uncaught main-process error.

Both listeners now return early once the window is destroyed, like the
zoom-changed listener. The quit shortcut handler still runs first in
before-input-event, since it guards its own window access and must see
the key-up.

Fixes #33614

AI-assisted; verified with the tests and tools listed above.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Nov 10, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at a103cf6

Macroscope's review found this PR approvable — Adds guards that prevent keyboard and gesture events from accessing a destroyed desktop window during teardown. Normal input behavior remains unchanged, with no material operational or data risk identified.

No code changes detected at a364c2a. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The main window’s input handlers now return when the window is destroyed. A regression test checks that trailing Cmd+Q key-up and scroll-end events do not throw.

Changes: Destroyed-window input handling

Layer / File(s) Summary
Guard input handlers after destruction
apps/desktop/src/window/DesktopWindow.ts, apps/desktop/src/window/DesktopWindow.test.ts
Both input handlers return before processing events when the window is destroyed. The test verifies that trailing Cmd+Q key-up and scroll-end events do not throw.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

  • pingdotgg/t3code#25188: Adds macOS swipe handling to the input-event path. This PR adds a destruction guard to that path.

Suggested reviewers: yash-singh1

Merge Risk: Minimal · up to a364c

The input handlers guard destroyed windows, with no actionable merge-blocking risk identified.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly describes the fix for input events during window teardown. It is specific to the main change.
Description check Passed The description covers the problem, change, scope, and detailed verification. It also notes that macOS timing was not verified. Although it says AI assistance was used, it does not name the model and …
Linked Issues check Passed Issue #33614 requires a destroyed-window guard before before-input-event accesses window.webContents, plus a regression test. The reviewed summary reports that DesktopWindow.ts checks `window.is…
Out of Scope Changes check Passed The current PR summary identifies changes to the two DesktopWindow listeners and their regression test. These changes address issue #33614. The large incremental diff includes a merge of main; it do…
Full details: Description check

Explanation

The description covers the problem, change, scope, and detailed verification. It also notes that macOS timing was not verified. Although it says AI assistance was used, it does not name the model and harness requested by the template; the remaining content is complete.

Full details: Linked Issues check

Explanation

Issue #33614 requires a destroyed-window guard before before-input-event accesses window.webContents, plus a regression test. The reviewed summary reports that DesktopWindow.ts checks window.isDestroyed() after quitShortcutHandler and before the access. The test covers a trailing Cmd+Q key-up after destruction. The added input-event guard covers the same teardown failure in the related listener.

Full details: Out of Scope Changes check

Explanation

The current PR summary identifies changes to the two DesktopWindow listeners and their regression test. These changes address issue #33614. The large incremental diff includes a merge of main; it does not establish unrelated changes in the PR diff from the supplied merge base.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 6512788 and a364c2a.

Files selected for processing: 2

  • apps/desktop/src/window/DesktopWindow.test.ts
  • apps/desktop/src/window/DesktopWindow.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed labels Nov 10, 2026
@juliusmarminge juliusmarminge added pr:in-progress A lead is working on this PR and removed pr:gate-failed A land gate failed labels Nov 10, 2026
@juliusmarminge juliusmarminge added pr:needs-verification Reviewed and green; needs a run on a desktop, Android or iOS device and captures posted and removed pr:in-progress A lead is working on this PR labels Nov 10, 2026

Copy link
Copy Markdown
Member Author

Evidence

Built and ran both revisions as real Electron desktop apps in separate worktrees. Every capture uses Linux x86_64, Electron 44.10.1, Xvfb 1920×1080, a 1100×780 app window, light theme, and fresh isolated state with imports disabled.

With quit mode Hold, focus the workspace, wait two seconds, press Ctrl-Q twice using xdotool key --delay 40 ctrl+q ctrl+q, then observe for five seconds. The identical driver produces the destroyed-object exception on base and a clean quit on head (exit status 0).

Before After
5be6bc99c2 a364c2af19
Rapid double Ctrl-Q recording: uncaught main-process TypeError: Object has been destroyed dialog. Identical rapid double Ctrl-Q recording: quits without an error dialog.
Held Ctrl-Q recording: clean quit. Held Ctrl-Q recording: clean quit.

The held-key comparison used identical 300 ms / 30 Hz repeat settings and a two-second hold; it did not reproduce the bug on base. Internal stack paths are masked in the recordings; the exception text remains visible.

Interaction driver · Desktop launcher · Setup and observations

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Native macOS Command-Q timing was not verified because this environment runs Linux; the demonstrated trigger uses Ctrl-Q.
  • The held-key variant did not reproduce the exception on base: both revisions quit cleanly under the identical scripted hold.
  • Late native gesture events and the input-event gesture guard were not exercised; the demonstrated failure covers keyboard input.
  • Fetching origin failed with HTTP 503 (fatal: expected 'acknowledgments'); the base was resolved using the available local origin/main reference.

@juliusmarminge
juliusmarminge merged commit 3725817 into main Nov 10, 2026
34 checks passed
@juliusmarminge
juliusmarminge deleted the fix/issue-33614 branch November 10, 2026 21:59
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Nov 11, 2026
**v0.1.9-nightly.20261110.4491** · 40 changes from 9 contributors

- **Shared OpenCode goals.** OpenCode goals stay in sync across clients.
- **OpenCode checklists.** See OpenCode todo updates as checklists.
- **Drag threads into the composer.** Drag threads from the command palette into the composer as context without navigating away.
- **More usage limits.** See Muse Code limits and xAI and Antigravity accounts managed through CLIProxyAPI in Usage → Limits.
- **Open apps from links.** Open apps from Markdown links and see their icons.
- **Checkpoint new files.** Checkpoints retain new files when ignore files contain blank lines.
- **Open cited files.** Open more file links from chat, including citations with line ranges.

### Plus 30 more bug fixes and performance improvements.

<details>
<summary>Full changelog</summary>

## What's Changed
* fix(mobile): iOS 18 iPad sidebar and thread feed start below the header by @juliusmarminge in pingdotgg/t3code#32936
* fix(web): Tab reaches the add-project and new-project submit buttons by @juliusmarminge in pingdotgg/t3code#32605
* fix(web): screen readers hear the add-project submit shortcut by @juliusmarminge in pingdotgg/t3code#32576
* fix(server): Antigravity limits read the quota host the account is metered on by @juliusmarminge in pingdotgg/t3code#32425
* fix(web): the thread menu works for an older settled thread the sidebar hasn't loaded by @juliusmarminge in pingdotgg/t3code#32507
* fix(opencode): show questions asked while OpenCode wakes a session on its own by @juliusmarminge in pingdotgg/t3code#32434
* fix(server): blank .gitignore lines no longer make checkpoints drop new files by @juliusmarminge in pingdotgg/t3code#32374
* test(web): minimap next-turn test no longer flakes after a resize by @t3dotgg in pingdotgg/t3code#33964
* feat(opencode): mirror native goals across clients by @Yash-Singh1 in pingdotgg/t3code#33758
* feat(web): bindable shortcut to regenerate a thread's title by @sharanry in pingdotgg/t3code#33987
* fix(server): linked projects keep their repository identity while it refreshes by @juliusmarminge in pingdotgg/t3code#32359
* fix(mobile): tool call commands are highlighted and monospaced like on web by @juliusmarminge in pingdotgg/t3code#33957
* fix: Claude monitor wakes no longer fire a completion alert per line by @juliusmarminge in pingdotgg/t3code#32520
* fix(server): removed thread worktrees keep their project's skills in the picker by @juliusmarminge in pingdotgg/t3code#32471
* fix(web): drag command palette threads into the composer by @Yash-Singh1 in pingdotgg/t3code#33990
* fix(shared): show zero seconds in minute durations by @Yash-Singh1 in pingdotgg/t3code#33994
* fix(web): closing the right panel returns focus to where it was by @juliusmarminge in pingdotgg/t3code#32569
* fix(server): say why threads without a project are unavailable by @juliusmarminge in pingdotgg/t3code#32342
* fix: chat file links open the file agents cite by @juliusmarminge in pingdotgg/t3code#33943
* fix(clients): scroll truncated command output notice with logs by @Yash-Singh1 in pingdotgg/t3code#34011
* fix(web): show app bundle icons and open apps from markdown links by @Yash-Singh1 in pingdotgg/t3code#33950
* feat(opencode): project native todo updates into checklists by @Yash-Singh1 in pingdotgg/t3code#34000
* fix(antigravity): recognize Gemini 4 Carbon and its quota by @DrSeedon in pingdotgg/t3code#33991
* perf(client): reopening a finished command row no longer refetches its output by @juliusmarminge in pingdotgg/t3code#34002
* perf(server): user-only message reads use the user-message index for ordinals by @juliusmarminge in pingdotgg/t3code#33009
* fix(web): keep HTML previews anchored when accordions expand by @Yash-Singh1 in pingdotgg/t3code#34003
* fix(web): preserve search result titles in drag previews by @Yash-Singh1 in pingdotgg/t3code#34017
* fix: file links with line ranges open at their first line by @juliusmarminge in pingdotgg/t3code#34006
* fix(codex): show image generation tool calls by @Yash-Singh1 in pingdotgg/t3code#34020
* fix(pi): a steer Pi has not read yet shows as waiting, not steered by @juliusmarminge in pingdotgg/t3code#33982
* fix(server): provider switches hand off started cancelled runs by @juliusmarminge in pingdotgg/t3code#34016
* fix(web,mobile): replace sparkles icons and ban them with a lint rule by @flamboh in pingdotgg/t3code#25422
* fix(github): a proxy's 403 with Retry-After is no longer reported as a rate limit by @juliusmarminge in pingdotgg/t3code#33676
* fix(web): sending from an empty citation comment clears the composer by @juliusmarminge in pingdotgg/t3code#33440
* fix(server): text generation fallback respects project provider settings by @Muzammil-Bit in pingdotgg/t3code#33741
* feat(usage): show Muse Code limits, and xAI and Antigravity accounts from a CLIProxyAPI hub by @StiensWout in pingdotgg/t3code#33800
* fix(web): preserve previews during attachment preparation by @Yash-Singh1 in pingdotgg/t3code#33949
* fix(desktop): input during window teardown no longer throws in the main process by @juliusmarminge in pingdotgg/t3code#33620
* fix(web): sent messages keep the numbers written on list items by @tsapa44 in pingdotgg/t3code#33820
* fix(server): quitting stops an agent-device daemon left by an earlier server by @juliusmarminge in pingdotgg/t3code#33587

## New Contributors
* @sharanry made their first contribution in pingdotgg/t3code#33987
* @tsapa44 made their first contribution in pingdotgg/t3code#33820

**Full Changelog**: pingdotgg/t3code@v0.1.9-nightly.20261110.4478...v0.1.9-nightly.20261110.4491

</details>


Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.1.9-nightly.20261110.4491
fancyboi999 added a commit to fancyboi999/t3code that referenced this pull request Nov 11, 2026
…contrast

* origin/main: (480 commits)
  fix(scripts): scenarios run where localhost resolves to IPv6 first (pingdotgg#34121)
  feat(opencode): track native image job outcomes and previews (pingdotgg#34089)
  fix(web): scroll project picker to top on first result (pingdotgg#34113)
  feat(server): voice models reuse copies already on disk (pingdotgg#34096)
  fix(desktop): desktop IPC only answers the main app window (pingdotgg#34094)
  feat(claude): Fable resets are offered and redeemed apart from weekly resets (pingdotgg#34106)
  feat(scripts): scenarios prove a fix in the real app with e2e, on web and iOS (pingdotgg#34007)
  fix(web): question panel header text sits centered in its row (pingdotgg#34095)
  fix(server): a t3 thread send to a finished delegated child notifies its parent (pingdotgg#33446)
  feat(mobile): pick a voice environment once, then its method (pingdotgg#34073)
  fix(mobile): attaching larger images no longer crashes with a BigInt error (pingdotgg#34087)
  fix(server): quitting stops an agent-device daemon left by an earlier server (pingdotgg#33587)
  fix(web): sent messages keep the numbers written on list items (pingdotgg#33820)
  fix(desktop): input during window teardown no longer throws in the main process (pingdotgg#33620)
  fix(web): preserve previews during attachment preparation (pingdotgg#33949)
  feat(usage): show Muse Code limits, and xAI and Antigravity accounts from a CLIProxyAPI hub (pingdotgg#33800)
  fix(server): text generation fallback respects project provider settings (pingdotgg#33741)
  fix(web): sending from an empty citation comment clears the composer (pingdotgg#33440)
  fix(github): a proxy's 403 with Retry-After is no longer reported as a rate limit (pingdotgg#33676)
  fix(web,mobile): replace sparkles icons and ban them with a lint rule (pingdotgg#25422)
  ...

# Conflicts:
#	pnpm-lock.yaml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:needs-verification Reviewed and green; needs a run on a desktop, Android or iOS device and captures posted size:XS 0-9 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quitting with a held ⌘Q can throw "Object has been destroyed" in the main process

1 participant